Skip to content

feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (description-mode)#590

Merged
steipete merged 3 commits into
openclaw:mainfrom
mvanhorn:feat/calendar-with-zoom
May 17, 2026
Merged

feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (description-mode)#590
steipete merged 3 commits into
openclaw:mainfrom
mvanhorn:feat/calendar-with-zoom

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented May 16, 2026

Summary

gog calendar can now create, regenerate, and remove Zoom video conferences as a flag family parallel to the existing --with-meet surface. Adds gog zoom auth setup and gog zoom auth doctor for Zoom Server-to-Server OAuth credential storage. Closes #589.

The Zoom information is attached to the Calendar event description rather than conferenceData. The original direct-write architecture (conferenceSolution.key.type="addOn") does not work for non-Workspace-Marketplace OAuth clients; see "Architecture" below for the empirical findings.

Why this matters

"Today an agent can attach Google Meet links via gog in one tool call. When a client requests Zoom, the agent has no programmatic surface - the Zoom for Google Workspace add-on is UI-only and can't be triggered via the Calendar API's conferenceData.createRequest (or at least, not reliably across third-party add-ons)." - @alexisperumal in #589

The Zoom for Google Workspace add-on is UI-only. agent-plex/openclaw-zoom covers Team Chat (different product); AIGC-Hackers/openclaw-zoom-agent joins existing meetings (does not create or schedule). The "agent schedules a Zoom meeting and attaches it to a Google Calendar event" niche is uncovered upstream.

Architecture

The PR originally claimed that conferenceData could be written directly with conferenceSolution.key.type="addOn" from a vanilla OAuth client. @alexisperumal's live-capture testing against real Zoom Pro + real Google Calendar accounts disproved this. A follow-up matrix of empirical tests against a real Google Calendar account confirmed the deeper finding:

Variant Result
key.type="addOn" + name + iconUri + entryPoints 400 "Invalid conference data"
Same + parameters.addOnParameters.parameters.scriptId (any value, including AKfycb-shaped) 400 "Invalid conference data"
Same + entryPoint.passcode + conferenceData.notes 400 "Invalid conference data"
All of the above combined 400 "Invalid conference data"
Drop key.type, keep name + iconUri + entryPoints 200 OK, but conferenceData silently stripped on storage
Just entryPoints + conferenceId, no conferenceSolution 200 OK, but conferenceData silently stripped on storage

Google's Calendar API enforces conferenceSolution.key.type="addOn" as a privileged claim that requires the calling OAuth client identity to belong to a registered Workspace Marketplace Conference Add-on. Without that registration, any conferenceData payload that asserts the claim is rejected, and any payload that omits the claim is dropped during storage.

The realistic salvage is description mode: gog creates the Zoom meeting via Zoom API, then writes the join URL + meeting ID + passcode into the Calendar event description inside HTML-comment markers. The event renders as a clickable Zoom link in every Calendar UI; the trade-off is no native "Join with Zoom" conference card.

Changes

  • New internal/zoom/ package: minimum-viable Zoom Meetings client (S2S OAuth token exchange + cached refresh, POST /users/{userId}/meetings, DELETE /meetings/{meetingId}). Production transport is http.DefaultTransport; tests inject a http.RoundTripper. No InsecureSkipVerify.

  • New gog zoom auth setup / gog zoom auth doctor subcommands. User-level OAuth scopes (meeting:write, meeting:read, user:read) when available; :admin granular variants when the Marketplace UI exposes only those.

  • Secret storage reuses internal/secrets with namespaced keys (zoom-account/<alias>/client-secret, zoom-account/<alias>/access-token). Non-secret account metadata lives at ~/.config/gogcli/zoom/<alias>.json (dir 0700, file 0600). No second keyring.

  • New internal/cmd/zoom_description.go helpers: build, replace, remove, and parse the gog-managed Zoom block in event descriptions. Block format:

    <!-- gog-zoom-meeting:<meetingID> -->
    Join Zoom Meeting: <joinURL>
    Meeting ID: <id>
    Passcode: <pwd>   [if present]
    <!-- /gog-zoom-meeting -->
    
  • buildConferenceData Zoom branch returns nil (no conferenceData on the Zoom path); buildZoomConferenceData removed. Meet path is unchanged.

  • extractZoomMeetingID now parses the description block first and falls back to legacy conferenceData parsing for events created by the Zoom for Google Workspace add-on or other tools.

  • eventConferenceProvider detects the gog Zoom block in the description before inspecting conferenceData, so cross-provider mutex checks behave correctly for both shapes.

  • Flag-mutex matrix enforced at parse time for the seven incompatible pairs in calendar_edit.go (--with-zoom + --regenerate-zoom, etc.). Unchanged.

  • Cross-provider runtime check: --with-zoom on an event that already has a Meet conference returns usage("event already has a Meet conference; use --remove-meet first, then --with-zoom"). (--remove-meet itself is out of scope for this PR; see "Deferred".)

  • Cancel-before-create failure semantics for --regenerate-zoom: cancel old meeting -> on 404/410 proceed; on other error abort, Calendar event unchanged. Create new meeting -> patch Calendar description. If the Calendar patch fails, the newly-created Zoom meeting is cancelled.

  • --remove-zoom cancels the Zoom meeting (404/410 = success, other error = stderr warning + proceed), strips the gog Zoom block from the description, and (for backward compatibility) also clears any legacy conferenceData that looks like a Zoom conference.

  • redactZoomURL in internal/zoom/redact.go masks the pwd= query parameter on every join URL reaching stdout/stderr. --include-passwords flag (or GOG_ZOOM_INCLUDE_PASSWORDS=1) opts out for debugging. The passcode in the description block is intentionally rendered as the standalone Passcode: <pwd> line.

  • Every destructive Zoom API call writes a structured audit line to stderr: [zoom] meeting=<id> action=<cancel|delete|regenerate> ts=<rfc3339> cmd=<argv0>.

  • conferenceDataVersion=1 is no longer sent for --with-zoom (description-mode does not mutate conferenceData); still sent for --with-meet and for --remove-zoom when a legacy conferenceData field is being cleared.

Testing

  • Unit tests in internal/cmd/calendar_zoom_test.go and internal/cmd/calendar_create_update_test.go, all 16 Zoom-specific tests pass.
  • Empirical end-to-end testing against a real Google Calendar account confirmed the architectural finding above. The full matrix is reproducible against any Google account by setting GOGCLI_DEBUG_MODE and GOGCLI_DEBUG_SCRIPT_ID env vars on the binary; the experimental gates were removed before this commit, but the methodology is recorded for posterity.

Deferred to follow-up PRs

To keep this PR close to the project's median size:

  • Standalone gog zoom meeting create|get|list|update|delete subtree.
  • --remove-meet (real gap, but adjacent to this work).
  • Optional --zoom-no-notes and --zoom-host <email> flags (latter needs the *:admin scope split).
  • gog zoom auth tokens subcommand.
  • Registering gogcli as a Workspace Marketplace Conference Add-on so the native "Join with Zoom" card renders. Large scope (Apps Script project, OAuth review, Marketplace listing).

Open Questions answered in #589

  1. Flag naming: chose the concrete --with-zoom family over a generic --conference-provider zoom form. Rationale: discoverability + mirrors the existing --with-meet precedent. Happy to refactor if you prefer the abstraction.

  2. Implementation architecture: direct Zoom API write attached to the event description (path a in the issue, after the architectural revision documented above). Calendar createRequest passthrough (path b) was rejected because it depends on third-party add-ons implementing Google's Conference Add-on createConference callback - Zoom's add-on does not.

  3. addOnParameters server-side validation: empirically tested. Adding parameters.addOnParameters.parameters.scriptId does not unlock the key.type="addOn" write; Google validates the calling client identity rather than the payload contents.

@alexisperumal
Copy link
Copy Markdown

Live capture against real Zoom Pro + real Google Calendar — finding to report

Thanks for the fast turn on this, @mvanhorn — really appreciate the structured PR, the empirical Phase 0 framing, and the test coverage. Did the live capture exercise tonight against the real surface. Headline up front: the write fails Google's validation in our testing, and I want to share the evidence in case it's useful — but with the up-front caveat that we may be missing something you saw in Phase 0.

Test setup

  • PR branch: b954a67 built locally on macOS (go build -o gog-test ./cmd/gog)
  • Zoom Pro account with a fresh Server-to-Server OAuth app, scopes: meeting:write:meeting:admin, meeting:read:meeting:admin, meeting:delete:meeting:admin, user:read:user:admin (couldn't find user-level granular variants in marketplace.zoom.us for S2S app type — happy to dig more if you saw those somewhere)
  • Two Google accounts tested, both consumer @gmail.com accounts, both via the same Desktop-type Google Cloud OAuth client:
    1. Account A — calendar without Zoom for Google Workspace add-on installed
    2. Account B — calendar with the add-on installed and authorized

What works ✅

The bulk of the surface works exactly as documented:

  • Zoom S2S setup (gog zoom auth setup + gog zoom auth doctor) — clean, /users/me validated on first try
  • pwd= redaction in stderr — verified zero leaks (grep -cE 'pwd=[A-Za-z0-9._-]' on stderr returns 0)
  • Structured audit lines[zoom] meeting=<id> action=<...> ts=<rfc3339> cmd=<argv0> present on every destructive Zoom call
  • Flag-mutex matrix--with-zoom + --with-meet rejected at parse time with clean usage error (exit code 2). Worth noting --with-meet is the more user-likely surprise vs. intra-Zoom combos, so good coverage there.
  • Failure semantics for --with-zoom — when the Calendar write fails (see below), gog properly cancels the orphaned Zoom meeting via the cancel-on-error path. The audit log + my own check against the Zoom marketplace confirm the meeting was actually cancelled, not just marked locally.
  • Sanity check: --with-meet works fine through the same OAuth client setup — confirms the OAuth client + Calendar API basics are functional, isolates the issue to the Zoom write path.

What fails ❌

gog calendar create primary --with-zoom ... against both accounts:

[zoom] meeting=<id> action=delete ts=<rfc3339> cmd=./gog-test
Google API error (400 invalid): Invalid conference data.

Sequence: Zoom meeting created via Zoom API (success) → Calendar event POST with the addOn-shaped conferenceData (400 rejection from Google) → orphaned Zoom meeting cancelled (cleanup works correctly).

Reproducible across both Google accounts — including the one with the Zoom add-on already installed and authorized. Three runs, same failure each time.

Comparison: what gog generates vs. what the Zoom add-on actually produces

Pulled internal/cmd/calendar_build.go for the Zoom path:

return &calendar.ConferenceData{
    ConferenceId: meetingID,
    ConferenceSolution: &calendar.ConferenceSolution{
        Key:     &calendar.ConferenceSolutionKey{Type: "addOn"},
        Name:    "Zoom Meeting",
        IconUri: iconURI,
    },
    EntryPoints: []*calendar.EntryPoint{entry},  // entry has: video type, uri, label, meetingCode
}

Earlier (before this PR existed), I'd captured an actual Zoom add-on-produced event via gog calendar raw to seed the original feature request. Comparing the structures:

Field gog --with-zoom produces Zoom add-on produces
conferenceSolution.key.type "addOn" "addOn"
conferenceSolution.name "Zoom Meeting" "Zoom Meeting"
conferenceSolution.iconUri st1.zoom.us/.../ZoomLogo.png lh3.googleusercontent.com/.../...s128-no
entryPoints 1 (video) 4 (video, phone, sip, more)
entryPoint[*].passcode not set set
conferenceData.notes not set rich HTML block
parameters.addOnParameters.parameters.scriptId not set set (references the Apps Script add-on)

Hypothesis for the root cause

The parameters.addOnParameters.parameters.scriptId missing field looks like the most likely killer. Google's validation for conferenceSolution.key.type = "addOn" may require a valid scriptId referencing an authorized add-on for the calendar. A non-add-on OAuth client can't provide this.

This would explain why the Phase 6 differential (with-vs-without add-on installed) shows no behavior change — Google's validation gates on the write payload's contents (scriptId), not on the calendar's add-on installation state.

But hedging hard: this is one possible explanation, not a verified root cause. The actual Google error is the bare "Invalid conference data" string — no field-level detail. The maintainer's debugging path will be more informative than ours since you have the full request/response visibility.

What might we be missing? (genuine question)

A few things I'm uncertain about, listed in case any of them resolves the discrepancy:

  1. Was your Phase 0 test using gog's actual generated payload, or a hand-rolled minimal one? If the latter, the actual gog code path may include a field that triggers stricter validation. Recreating Phase 0 by running the gog binary itself (not a hand-rolled equivalent) would isolate this.
  2. Did your test calendar have any prior conferenceData that may have left cached state? Long shot, but Google's conferenceDataVersion=1 semantics interact oddly with prior state sometimes.
  3. Workspace vs. consumer-Gmail account differences? Both our test accounts are consumer Gmail (@gmail.com). If your test was on a Workspace org account, there may be admin-policy differences in conferenceData validation that don't apply to consumer accounts. (Long shot but worth ruling out.)
  4. Scope differences? We used :admin granular Zoom scopes (couldn't find user-level granular variants for S2S apps in the marketplace UI). If you tested with classic non-granular scopes, possibly different audit behavior. Unlikely to affect the Calendar API side, but mentioning for completeness.
  5. Different OAuth client type? Ours is a Desktop OAuth client. If yours was a Web OAuth client (or a service account), different validation may apply.

Tier 1 surface coverage status

Given the write fails, I can only validate the gog-side behavior, not the end-to-end success path. What I confirmed:

  • --with-zoom invokes the right code path (Zoom API call, audit log, error handling)
  • --regenerate-zoom and --remove-zoom: not exercised (need an existing Zoom event)
  • ✅ Mutex (--with-zoom + --with-meet): correctly rejected at parse time
  • ✅ Idempotency: not exercised
  • ✅ pwd= redaction: verified

One small doc gap to flag

The PR's docs/zoom-auth-setup.md assumes the reader already has gog's Google OAuth client JSON configured. For someone testing this PR fresh (no prior gog setup), the Google OAuth client prerequisite is non-trivial: requires a Google Cloud project, OAuth consent screen configuration, OAuth client creation, and (since Google's recent policy change) a previously-downloaded credentials.json since secrets can't be re-downloaded. Worth a one-line cross-reference to gogcli's existing Google auth setup docs at the top of the Zoom auth doc.

Help offered

Happy to:

  • Re-run any variant tests with different flag combinations / verbosity levels / OAuth client types
  • Test against alternative architectures if you iterate (e.g., if you decide to populate dummy addOnParameters values to see if Google accepts a syntactically-complete payload from a non-add-on caller)
  • Capture more diagnostic detail with a network proxy if useful (could show the exact JSON Google's API receives)
  • Share the unsanitized terminal logs / payloads via DM if you want bytes-level evidence

Thanks again for the work on this — the engineering quality of the PR itself (test coverage, redaction, audit, atomic failure semantics) is excellent. Just want to surface what we found so you have it as you decide next steps.

mvanhorn added 2 commits May 16, 2026 23:02
@alexisperumal's live-capture test (openclaw#590 review) showed the original
conferenceData write fails Google's validation with 400 "Invalid conference
data". An expanded empirical matrix (recorded in the PR description and
CHANGELOG) showed:

- conferenceSolution.key.type="addOn" is rejected from any non-Workspace-
  Marketplace OAuth client regardless of payload (scriptId, passcode, notes
  are incidental)
- Omitting key.type makes the POST succeed but Google silently strips the
  entire conferenceData field on storage

The only path to the native "Join with Zoom" card is registering gogcli as a
Workspace Marketplace Conference Add-on (large scope, separate PR).

This commit pivots --with-zoom to attach the Zoom join URL + meeting ID +
passcode to the Calendar event description inside HTML-comment markers. The
event renders a clickable Zoom link in every Calendar UI; --regenerate-zoom
replaces the block in place; --remove-zoom strips it (and clears any legacy
Zoom-shaped conferenceData for backward compatibility with events created by
the Zoom for Google Workspace add-on).

Trade-off: no native conference card.

- internal/cmd/zoom_description.go: build / apply / remove / extract helpers
  for the gog-managed Zoom block
- internal/cmd/calendar_build.go: Zoom branch returns nil; remove unused
  buildZoomConferenceData, zoomLabel, zoomConferenceIconURI
- internal/cmd/calendar_edit.go: create + with-zoom + regenerate-zoom +
  remove-zoom rewired to operate on event.Description; conferenceVersion=1
  no longer sent for the Zoom path
- internal/cmd/calendar_zoom.go: extractZoomMeetingID and
  eventConferenceProvider read the description block first, fall back to
  legacy conferenceData for events written by other tools
- internal/cmd/calendar_zoom_test.go: assertions updated to verify the
  description block shape and absence of conferenceData
- docs/zoom-auth-setup.md: rewritten to explain description mode and add the
  Google OAuth client prerequisite cross-reference @alexisperumal flagged
- CHANGELOG.md: 0.17.1 entry rewritten to describe description-mode
@mvanhorn mvanhorn force-pushed the feat/calendar-with-zoom branch from b954a67 to 53889e2 Compare May 17, 2026 06:03
@mvanhorn mvanhorn changed the title feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (description-mode) May 17, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Live capture finding confirmed, expanded matrix, and pivot to description mode

Thanks @alexisperumal - your live capture was correct, and pushed me to test the architecture properly. Phase 0's claim ("the direct-write architecture works") turns out to have been a misread of read-back behaviour vs write-acceptance from a non-add-on client. Apologies for the cycles.

I reproduced your 400 on the same payload, captured the rejected JSON body via a temporary debug hook, and then walked the broader matrix to figure out what Google actually validates. Posting the full table here in case it's useful for the next person who hits this surface:

Variant Result on POST conferenceData after storage
conferenceSolution.key.type="addOn" + name + iconUri + entryPoints (PR HEAD b954a67) 400 "Invalid conference data" (rejected)
+ parameters.addOnParameters.parameters.scriptId="" 400 (rejected)
+ scriptId="1234567890abcdef-fake-script-id" 400 (rejected)
+ scriptId="AKfycbxFakeScriptIdNotARealOne" 400 (rejected)
+ entryPoint.passcode + conferenceData.notes 400 (rejected)
All of the above combined 400 (rejected)
Drop key.type, keep conferenceSolution{name, iconUri} + entryPoints 200 silently stripped (read-back has no conferenceData)
Just entryPoints + conferenceId (no conferenceSolution) 200 silently stripped (read-back has no conferenceData)

The scriptId hypothesis isn't the variable - it's incidental. Google validates key.type="addOn" against caller identity (registered Workspace Marketplace Add-on or not) before looking at the payload, and silently drops the entire conferenceData on storage if key.type is absent. The only key.type value that survives a non-add-on write is "hangoutsMeet" via the existing createRequest mechanism (which is what --with-meet uses).

This makes the direct-write architecture unreachable from a vanilla OAuth client. The Workspace Marketplace Conference Add-on path (Apps Script project, OAuth review, Marketplace listing, real scriptId) is the only route to the native "Join with Zoom" card.

Pivot in 53889e2: description mode

I rewired the Zoom flag family to attach the join URL + meeting ID + passcode to the Calendar event description inside HTML-comment markers, rather than to conferenceData:

<!-- gog-zoom-meeting:86823956608 -->
Join Zoom Meeting: https://us06web.zoom.us/j/86823956608?pwd=...
Meeting ID: 86823956608
Passcode: <passcode>
<!-- /gog-zoom-meeting -->

The markers let --regenerate-zoom replace the block in place and --remove-zoom strip it without disturbing surrounding description content. extractZoomMeetingID parses the block start marker; for backward compatibility it still falls back to parsing conferenceData.entryPoints so events created by the real Zoom for Google Workspace add-on are still detected by the cross-provider check and by --regenerate-zoom/--remove-zoom.

conferenceDataVersion=1 is no longer sent for the Zoom path (we are not mutating conferenceData). The flag-mutex matrix and the cross-provider check (event already has a Meet conference; use --remove-meet first) are unchanged.

The trade-off, to call out explicitly: no native "Join with Zoom" conference card. The event renders a clickable Zoom link in every Calendar UI but as plain description text, not as the deeply-integrated card the registered add-on produces. Becoming a Marketplace Conference Add-on would lift that ceiling and I'd be happy to scope a follow-up if that's a direction you want.

Doc gap

Fixed in 53889e2 per your note: docs/zoom-auth-setup.md now has a Prerequisites section pointing readers at the gogcli quickstart for the Google OAuth client setup before reaching the Zoom-app steps.

Status

  • Branch rebased onto main to clear the merge conflict.
  • PR description rewritten to match the new architecture (and a couple of stale Phase 0 claims removed).
  • 16 Zoom-specific tests pass against the new shape; live --with-zoom against my own Google account succeeds and the description block survives the round-trip.
  • I removed the experimental GOGCLI_DEBUG_MODE / GOGCLI_DEBUG_SCRIPT_ID / GOGCLI_DEBUG_DUMP_EVENT gates before commit.

Two things I'd value your judgment on:

  1. Is description-mode the right product direction for feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (parity with --with-meet) #589's "agent attaches a Zoom meeting to a Calendar event" niche, or would you prefer the PR close and a Marketplace-Add-on rework take its place? The empirical finding is the same either way; only the response differs.
  2. Anything I should re-test on your live setup before another review pass? Happy to capture additional payloads with GOGCLI_DEBUG_DUMP_EVENT if you want to compare any specific shape against your test account.

@steipete steipete merged commit 5afc19f into openclaw:main May 17, 2026
5 checks passed
@alexisperumal
Copy link
Copy Markdown

Description-mode validated end-to-end — one edge-case bug found

Thanks @mvanhorn for the exhaustive matrix test and the rapid pivot, and @steipete for the merge. Re-ran the live capture against main HEAD (5afc19f) on the same setup as yesterday — Zoom Pro + consumer Gmail, no add-on installed.

TL;DR: description-mode works as designed in 4 of 5 cases I exercised; one edge-case --remove-zoom bug filed as separate issue (link mid-comment).

What works ✅

Surface Result
--with-zoom create Description block renders correctly: start/end markers, Join URL, Meeting ID, Passcode. No conferenceData (correct per architecture).
--regenerate-zoom Old Zoom meeting cancelled (audit line action=regenerate), new one created, description block replaced in place. Meeting ID changed as expected.
--remove-zoom on mixed-content event (user content + Zoom block) Block stripped correctly, surrounding user content preserved.
Audit lines Present + structured for both regenerate and delete.
Doc gap fix Confirmed — docs/zoom-auth-setup.md now has the Prerequisites section pointing at gogcli's Google OAuth client setup. Thanks.

Not re-tested today (unchanged from yesterday's pass): flag-mutex matrix, pwd= redaction, and legacy conferenceData-shape backward-compat (would require manually creating an event via the actual Zoom add-on first).

One bug — --remove-zoom on Zoom-only-description event ⚠️

When the event's description contains only the gog-managed Zoom block (no other user content), --remove-zoom cancels the Zoom meeting correctly but leaves the description block intact. Recipients would still see a clickable Zoom link pointing at a now-cancelled meeting.

Filed as a separate issue with reproduction steps, source citation, and a suggested fix direction: #591

TL;DR root cause: the merge logic at internal/cmd/calendar_edit.go:994-996 treats an empty patch description as "no change" rather than "clear it." Likely fix is patch.ForceSendFields = append(patch.ForceSendFields, "Description") when the post-strip result is empty (this is a pre-existing patch-merge gap that the description-mode path newly exposes — not a regression introduced by #590).

Answers to your two questions

  1. Is description-mode the right product direction? Yes, for our use case. The native "Join with Zoom" card would be marginally better UX, but the Marketplace Conference Add-on path (Apps Script project, OAuth verification, Marketplace listing) is a multi-week commitment for an incremental polish improvement. Description-mode is the pragmatic landing. If multiple users surface the same gap, the Marketplace route becomes more compelling — but absent that signal, the current approach is well-balanced.

  2. Anything to re-test on our live setup before another review pass? The merge happened before I could re-test, so this question is moot for the original PR — but the answer post-hoc is "yes, the empty-description --remove-zoom case" (now captured in the linked bug issue). Happy to re-validate any proposed fix against the same Zoom Pro + consumer Gmail setup; test infrastructure is still in place.

Thanks again for the iteration speed and the matrix-test investigation. The 8-row table you posted will be useful to anyone who hits this surface in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (parity with --with-meet)

3 participants